/*
 * Copyright 2022 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.foundation

import androidx.compose.foundation.gestures.Orientation
import androidx.compose.runtime.Stable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp

/**
 * Clips bounds of scrollable container on main axis while leaving space for background effects
 * (like shadows) on cross axis.
 *
 * @param orientation orientation of the scrolling
 */
@Stable
fun Modifier.clipScrollableContainer(orientation: Orientation) =
    then(
        if (orientation == Orientation.Vertical) {
            Modifier.clip(VerticalScrollableClipShape)
        } else {
            Modifier.clip(HorizontalScrollableClipShape)
        }
    )

/**
 * In the scrollable containers we want to clip the main axis sides in order to not display the
 * content which is scrolled out. But once we apply clipToBounds() modifier on such containers it
 * causes unexpected behavior as we also clip the content on the cross axis sides. It is unexpected
 * as Compose components are not clipping by default. The most common case how it could be
 * reproduced is a horizontally scrolling list of Cards. Cards have the elevation by default and
 * such Cards will be drawn with clipped shadows on top and bottom. This was harder to reproduce in
 * the Views system as usually scrolling containers like RecyclerView didn't have an opaque
 * background which means the ripple was drawn on the surface on the first parent with background.
 * In Compose as we don't clip by default we draw shadows right in place. We faced similar issue in
 * Compose already with Androids Popups and Dialogs where we decided to just predefine some constant
 * with a maximum elevation size we are not going to clip. We are going to reuse this technique
 * here. This will improve how it works in most common cases. If the user will need to have a larger
 * unclipped area for some reason they can always add the needed padding inside the scrollable area.
 */
internal val MaxSupportedElevation = 30.dp

private object HorizontalScrollableClipShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density,
    ): Outline {
        val inflateSize = with(density) { MaxSupportedElevation.roundToPx().toFloat() }
        return Outline.Rectangle(
            Rect(
                left = 0f,
                top = -inflateSize,
                right = size.width,
                bottom = size.height + inflateSize,
            )
        )
    }
}

private object VerticalScrollableClipShape : Shape {
    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density,
    ): Outline {
        val inflateSize = with(density) { MaxSupportedElevation.roundToPx().toFloat() }
        return Outline.Rectangle(
            Rect(
                left = -inflateSize,
                top = 0f,
                right = size.width + inflateSize,
                bottom = size.height,
            )
        )
    }
}
